package com.redxiii.tracplus.ejb.search;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.redxiii.tracplus.ejb.search.query.SimpleQuerySpec;

public class LuceneSearchManager implements SearchManager {

	private static final AtomicInteger INSTANCE_ID = new AtomicInteger(1);
	private static final Logger logger = LoggerFactory.getLogger(LuceneSearchManager.class);
	private static final ReentrantLock lock = new ReentrantLock();	// Lucene is not tread safe
	private static final Version VERSION = Version.LUCENE_35;
	public static final String INDEX_DIRECTORY = "/wiki-lucene-index/";
	
	private static LuceneSearchManager instance;
	
	private boolean initialized = false;
	private File indexPathDir;
	private Directory index;
	private StandardAnalyzer analyzer;
	
	private LuceneSearchManager() throws IOException {
		indexPathDir = new File(System.getProperty("jboss.server.temp.dir") + INDEX_DIRECTORY);
		
		logger.debug("Starting Lucene Index on ({}): {}", Integer.valueOf(INSTANCE_ID.getAndIncrement()), indexPathDir);
		
		if (!indexPathDir.exists()) {
			throw new IllegalStateException("Index path does not exist: " + indexPathDir);
		}
		
		this.index = new SimpleFSDirectory(indexPathDir);
		this.analyzer = new StandardAnalyzer(VERSION);
	}
	
	public static LuceneSearchManager getInstance() {
		try {
			lock.lock();
			
			if (instance == null)
				instance = new LuceneSearchManager();
			
			return instance;
		} catch (Exception e) {
			logger.error("Error creating Lucene Search",e);
		} finally {
			lock.unlock();
		}
		logger.debug("Unable to create Lucene Index");
		return null;
	}
	
	public boolean isInitialized() {
		return initialized;
	}
	
	public void delete() {
		try {
			lock.lock();
			initialized = false;
			FileUtils.deleteDirectory(indexPathDir);
			
			if (!indexPathDir.exists())
				indexPathDir.mkdir();
		} catch (Exception e) {
			logger.error("Error creating Lucene Search",e);
		} finally {
			lock.unlock();
		}
	}
	
	public boolean updateIndex(Iterator<Document> docs) {
		try {
			lock.lock();
			IndexWriter writer = new IndexWriter(index, new IndexWriterConfig(VERSION, analyzer));
			while (docs.hasNext()) {
				Document doc = docs.next();
				writer.addDocument(doc, analyzer);
			}
			writer.close();
		} catch (CorruptIndexException e) {
			logger.error("Error updating Lucene index",e);
		} catch (IOException e) {
			logger.error("Error updating Lucene index",e);
		} finally {
			lock.unlock();
		}
		return initialized;
			
	}

	public IndexWriter createIndexWriter() {
		try {
			return new IndexWriter(index, new IndexWriterConfig(VERSION, analyzer));
		} catch (CorruptIndexException e) {
			logger.error("Error updating Lucene index",e);
		} catch (LockObtainFailedException e) {
			logger.error("Error updating Lucene index",e);
		} catch (IOException e) {
			logger.error("Error updating Lucene index",e);
		}
		return null;
	}
	
	/* (non-Javadoc)
	 * @see com.redxiii.tracplus.ejb.search.SearchManager#buildQuery(com.redxiii.tracplus.ejb.search.query.SimpleQuerySpec)
	 */
	@Override
	public Query buildQuery(SimpleQuerySpec spec) {

		BooleanQuery mainQuery = new BooleanQuery();
		
		Set<TracStuffField> fields = spec.getQueryFields();
		for (TracStuffField field : fields) {
			Set<String> values = spec.getValuesRestriction(field);
			for (String value : values) {
				
				if (value.length() < 3)
					continue;
				
				BooleanClause.Occur occur = BooleanClause.Occur.SHOULD;
				if (spec.isStrongRestriction(field, value))
					occur = Occur.MUST;
				
				
				Term term = new Term(field.name(), value);
				Query query = null;
				if (spec.isLikeRestriction(field, value))
					query = new FuzzyQuery(term, spec.getLikeRestrictionWeight(field, value));
				else
					query = new TermQuery(term);
				
				
				mainQuery.add(query, occur);
			}
		}
		
		if (spec.isRecentFilterEnable()) {
			DateTime start = new DateTime().withMillisOfDay(0).minusDays(spec.getRecentFilterDays());
			
			Query query = NumericRangeQuery.newLongRange(
					TracStuffField.MODIFIED_TIMESTAMP.name(), start.getMillis(), System.currentTimeMillis(), true, true);
			
			mainQuery.add(query, BooleanClause.Occur.MUST);
		}
		
		if (spec.getLuceneQuery() != null) {
			mainQuery.add(parseLuceneQuery(spec.getLuceneQuery()), Occur.SHOULD); 
		}
		
		return mainQuery;
	}
	

	private Query parseLuceneQuery(String lucene) {
		try {
			return new QueryParser(VERSION, TracStuffField.CONTENT.name(), analyzer).parse(lucene);
		} catch (ParseException e) {
			logger.error("Error creating query", e);
		}
		return null;
	}
	
	/* (non-Javadoc)
	 * @see com.redxiii.tracplus.ejb.search.SearchManager#doSearch(com.redxiii.tracplus.ejb.search.query.SimpleQuerySpec)
	 */
	@Override
	public Set<SearchResult> doSearch(SimpleQuerySpec spec) {

		Query query = buildQuery(spec);

		return doSearch(query, spec.getMaxHits());
	}
	
	/* (non-Javadoc)
	 * @see com.redxiii.tracplus.ejb.search.SearchManager#doSearch(org.apache.lucene.search.Query, int)
	 */
	@Override
	public Set<SearchResult> doSearch(Query query, int maxResults) {
		
		logger.info("Query: '{}'", query);
		Set<SearchResult> results = new LinkedHashSet<SearchResult>();

		if (query == null)
			return results;

		IndexSearcher searcher = null;
		IndexReader reader = null;
		try {
			reader = IndexReader.open(index, true);
			
			searcher = new IndexSearcher(reader) ;
			TopScoreDocCollector collector = TopScoreDocCollector.create(maxResults, true);
			searcher.search(query, collector);

			ScoreDoc[] hits = collector.topDocs().scoreDocs;
			for (ScoreDoc hit : hits) {
				Document doc = searcher.doc(hit.doc);
				results.add(new LuceneResult(doc, hit.score));
			}
		} catch (CorruptIndexException e) {
			logger.error("Error searching Lucene index", e);
		} catch (IOException e) {
			logger.error("Error searching Lucene index", e);
		} finally {
			if (searcher != null) {
				try {
					searcher.close();
				} catch (IOException e) {
					logger.error("Error closing Lucene index", e);
				}
			}
			if(reader != null) {
	            try {
	                reader.close();
	            } catch(IOException e) {
	                logger.error("Error closing Lucene index", e);
	            }
	        }
		}

		return results;
		
	}
	
	
	
	
}
